Skip to content

Fix use-after-free during lazy object initialization - #22401

Open
iliaal wants to merge 2 commits into
php:PHP-8.4from
iliaal:fix/gh-22399-lazy-revert-double-free
Open

Fix use-after-free during lazy object initialization#22401
iliaal wants to merge 2 commits into
php:PHP-8.4from
iliaal:fix/gh-22399-lazy-revert-double-free

Conversation

@iliaal

@iliaal iliaal commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Lazy object initialization destroyed property values while the slot or the dynamic-properties HashTable still aliased them, so a value whose __destruct reaches back through a reference cycle (unset($this->obj->prop)) double-freed it, a UAF in zend_objects_store_del. The fix clears the slot and detaches the properties table before dropping the refcount, across all four destruction sites in zend_lazy_object_revert_init and zend_lazy_object_init_proxy (declared and dynamic properties, ghost revert and proxy cleanup); verified under ASAN.

Fixes GH-22399

@morrisonlevi morrisonlevi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't really comment about the fix but here's one small thing I did notice.

Comment thread Zend/zend_lazy_objects.c Outdated
zval garbage;
ZVAL_COPY_VALUE(&garbage, p);
ZVAL_UNDEF(p);
i_zval_ptr_dtor(&garbage);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zend_lazy_object_revert_init is entirely off the hot path, right? I think zval_ptr_dtor would be more appropriate to avoid excess code size?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, switched both new dtors to zval_ptr_dtor.

zend_lazy_object_revert_init() and zend_lazy_object_init_proxy() dropped a
property value's refcount while the slot or dynamic-properties table still
aliased it, so a value whose destructor reaches back through a reference
cycle (unset($this->obj->prop)) freed it twice. Clear the slot and detach
the properties table before dropping the refcount.

Fixes phpGH-22399
@iliaal
iliaal force-pushed the fix/gh-22399-lazy-revert-double-free branch from d924213 to 7a36177 Compare June 23, 2026 22:26
@iliaal
iliaal requested a review from morrisonlevi June 24, 2026 11:55
@morrisonlevi

morrisonlevi commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@iliaal With Claude's help I found that this same issue happens for ghosts and proxies in another place. Pushed two test cases and a fix. The comments are a bit verbose, I might cut them down, but they explain how the VM state is being altered which is critical for me, I can't follow these bugs without these kinds of comments!

@iliaal

iliaal commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@iliaal With Claude's help I found that this same issue happens for ghosts and proxies in another place. Pushed two test cases and a fix. The comments are a bit verbose, I might cut them down, but they explain how the VM state is being altered which is critical for me, I can't follow these bugs without these kinds of comments!

Don't mind them personally, I find them more helpful and being inside a test hardly harmful to code flow

@iliaal

iliaal commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

The extra fix makes sense to me, thanks +1 from me on that bit

Comment thread Zend/zend_lazy_objects.c
&& ((obj->ce->ce_flags & ZEND_ACC_FINAL) || (prop_info->flags & ZEND_ACC_FINAL))) {
continue;
}
zend_object_dtor_property(obj, p);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIU zend_object_dtor_property() was extracted to a function for lazy objects. With these cases outlined, only the original caller remains. It might make more sense to outline that one as an optimization, and switch this to zval_ptr_dtor() in zend_object_dtor_property().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, it landed in the Lazy objects commit (58aa6fc) and zend_object_std_dtor() is now its only caller.

The lazy paths can't reuse it as-is though: they have to null the slot before dropping the refcount (that's the actual fix), and they already hold prop_info, while zend_object_dtor_property() re-derives it from the slot pointer. Sharing it would mean changing its signature.

Did you mean inlining it into zend_object_std_dtor() as single-caller cleanup, or reworking it so the lazy paths can share? Either way I'd keep that on master and leave this 8.4 PR as the minimal UAF fix, unless you'd prefer it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lazy object revert init with object destructor can lead to double free

3 participants